import os
import subprocess
import logging
from pathlib import Path
import re
import urllib.parse  # 导入URL编码模块
import json
import traceback
from datetime import datetime
import tempfile
import stat
import time

logger = logging.getLogger(__name__)

class GitService:
    @staticmethod
    def _get_auth_url(repo_url, access_token):
        """将普通的Git URL转换为带有访问令牌的URL
        
        Args:
            repo_url: 原始仓库URL
            access_token: Gitee私人访问令牌
        
        Returns:
            str: 带有访问令牌的URL
        """
        if not access_token:
            return repo_url
            
        # 如果已经是oauth2格式，直接返回
        if 'oauth2:' in repo_url:
            return repo_url
            
        # 处理HTTPS URL
        if repo_url.startswith('https://'):
            # 移除可能存在的用户名密码
            url_parts = repo_url.split('@')
            base_url = url_parts[-1] if len(url_parts) > 1 else repo_url.replace('https://', '')
            return f'https://oauth2:{access_token}@{base_url}'
            
        return repo_url

    @staticmethod
    def setup_ssh_key(ssh_key):
        """设置SSH密钥并返回SSH密钥文件路径
        
        Args:
            ssh_key: SSH私钥内容
            
        Returns:
            tuple: (密钥文件路径, 临时目录对象)
        """
        if not ssh_key:
            return None, None
            
        try:
            # 创建临时目录来存储SSH密钥
            temp_dir = tempfile.mkdtemp()
            ssh_dir = os.path.join(temp_dir, '.ssh')
            os.makedirs(ssh_dir, mode=0o700)
            
            # 写入SSH密钥
            key_path = os.path.join(ssh_dir, 'id_rsa')
            with open(key_path, 'w') as f:
                f.write(ssh_key)
            
            # 设置正确的权限
            os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR)
            
            # 创建SSH配置文件
            ssh_config_path = os.path.join(ssh_dir, 'config')
            with open(ssh_config_path, 'w') as f:
                f.write("""Host *
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
""")
            
            return key_path, temp_dir
        except Exception as e:
            logger.error(f"设置SSH密钥时出错: {str(e)}")
            return None, None

    @staticmethod
    def cleanup_ssh_key(temp_dir):
        """清理临时SSH密钥文件
        
        Args:
            temp_dir: 临时目录路径
        """
        if temp_dir and os.path.exists(temp_dir):
            import shutil
            try:
                shutil.rmtree(temp_dir)
            except Exception as e:
                logger.error(f"清理SSH密钥文件时出错: {str(e)}")

    @staticmethod
    def _get_default_git_email(username):
        """根据用户名生成默认的Git邮箱
        
        Args:
            username: Git用户名或Gitee用户名
            
        Returns:
            str: 生成的邮箱地址
        """
        # 移除特殊字符，只保留字母、数字和下划线
        safe_username = re.sub(r'[^a-zA-Z0-9_]', '', username)
        return f"{safe_username}@gitee.com"

    @staticmethod
    def execute_git_command(repo_path, command, project_credentials=None):
        """执行Git命令并返回结果
        
        Args:
            repo_path: 仓库路径
            command: 要执行的Git命令
            project_credentials: 项目凭证信息，格式为 {'access_token': 'xxx', 'git_user_name': 'xxx', 'git_user_email': 'xxx'}
        """
        temp_dir = None
        try:
            logger.info(f"Executing git command in {repo_path}: {command}")
            
            # 处理远程仓库URL和本地路径的区别
            processed_path = repo_path
            
            # 检查是否是远程仓库URL
            if re.match(r'^https?://', processed_path) or re.match(r'^git@', processed_path):
                logger.info(f"检测到远程仓库URL: {processed_path}")
                return {
                    "success": False, 
                    "message": f"请使用项目的本地仓库路径，而不是远程URL: {processed_path}",
                    "data": {
                        "original_path": repo_path
                    }
                }
            
            if processed_path.endswith('.git'):
                processed_path = processed_path[:-4]
                logger.info(f"处理后的仓库路径: {processed_path}")
            
            if not os.path.exists(processed_path):
                return {
                    "success": False, 
                    "message": f"仓库路径不存在: {processed_path}",
                    "data": {
                        "original_path": repo_path,
                        "processed_path": processed_path
                    }
                }
            
            if not os.path.exists(os.path.join(processed_path, '.git')):
                return {
                    "success": False, 
                    "message": f"无效的Git仓库: {processed_path}",
                    "data": {
                        "original_path": repo_path,
                        "processed_path": processed_path
                    }
                }
            
            # 准备环境变量
            env = os.environ.copy()
            env['GIT_TERMINAL_PROMPT'] = '0'
            env['GIT_ASKPASS'] = 'echo'
            
            # 如果是提交相关的命令，确保设置了用户名和邮箱
            if 'commit' in command and project_credentials:
                git_user_name = project_credentials.get('git_user_name') or project_credentials.get('git_username')
                if git_user_name:
                    # 设置用户名
                    subprocess.run(
                        f'git config user.name "{git_user_name}"',
                        shell=True,
                        cwd=processed_path,
                        env=env
                    )
                    
                    # 获取或生成邮箱
                    git_user_email = (
                        project_credentials.get('git_user_email') or 
                        GitService._get_default_git_email(git_user_name)
                    )
                    
                    # 设置邮箱
                    subprocess.run(
                        f'git config user.email "{git_user_email}"',
                        shell=True,
                        cwd=processed_path,
                        env=env
                    )
                    logger.info(f"已设置Git提交者信息: {git_user_name} <{git_user_email}>")
            
            # 如果命令涉及远程操作且提供了访问令牌，修改远程URL
            remote_url = None
            if project_credentials and project_credentials.get('access_token'):
                access_token = project_credentials['access_token']
                if any(cmd in command for cmd in ['push', 'pull', 'fetch', 'clone']):
                    # 获取当前远程URL
                    remote_url_process = subprocess.Popen(
                        'git config --get remote.origin.url',
                        shell=True,
                        cwd=processed_path,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        text=True,
                        env=env
                    )
                    remote_url, _ = remote_url_process.communicate()
                    remote_url = remote_url.strip()
                    
                    if remote_url:
                        # 设置新的远程URL
                        auth_url = GitService._get_auth_url(remote_url, access_token)
                        subprocess.run(
                            f'git remote set-url origin "{auth_url}"',
                            shell=True,
                            cwd=processed_path,
                            env=env
                        )
                        logger.info("已更新远程URL以使用访问令牌")
            
            process = subprocess.Popen(
                command,
                shell=True,
                cwd=processed_path,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                env=env
            )
            stdout, stderr = process.communicate()
            
            # 如果使用了访问令牌，恢复原始URL
            if project_credentials and project_credentials.get('access_token') and remote_url:
                subprocess.run(
                    f'git remote set-url origin "{remote_url}"',
                    shell=True,
                    cwd=processed_path,
                    env=env
                )
            
            # 过滤掉可能包含凭证信息的输出
            safe_stdout = GitService._sanitize_git_output(stdout)
            safe_stderr = GitService._sanitize_git_output(stderr)
            
            if process.returncode != 0:
                return {
                    "success": False, 
                    "message": f"Git命令执行失败: {safe_stderr}", 
                    "data": {"stdout": safe_stdout, "stderr": safe_stderr}
                }
            
            return {
                "success": True,
                "message": "命令执行成功",
                "data": {"stdout": safe_stdout, "stderr": safe_stderr}
            }
        except Exception as e:
            logger.error(f"Git命令执行异常: {str(e)}")
            return {"success": False, "message": f"Git命令执行异常: {str(e)}", "data": None}
        finally:
            # 清理临时SSH密钥文件
            if temp_dir:
                GitService.cleanup_ssh_key(temp_dir)

    @staticmethod
    def git_status(repo_path, project_credentials=None):
        """获取仓库状态"""
        return GitService.execute_git_command(repo_path, "git status", project_credentials)
    
    @staticmethod
    def git_add_all(repo_path, project_credentials=None):
        """添加所有更改到暂存区"""
        return GitService.execute_git_command(repo_path, "git add .", project_credentials)
    
    @staticmethod
    def git_commit(repo_path, message, project_credentials=None):
        """提交更改"""
        if not message:
            return {"success": False, "message": "提交信息不能为空", "data": None}
        
        # 确保提交信息包含在引号中，避免命令注入
        message = message.replace("'", "\\'").replace('"', '\\"')
        return GitService.execute_git_command(repo_path, f'git commit -m "{message}"', project_credentials)
    
    @staticmethod
    def git_commit_all(repo_path, message, project_credentials=None):
        """添加所有更改并提交"""
        if not message:
            return {"success": False, "message": "提交信息不能为空", "data": None}
        
        # 确保提交信息包含在引号中，避免命令注入
        message = message.replace("'", "\\'").replace('"', '\\"')
        return GitService.execute_git_command(repo_path, f'git commit -am "{message}"', project_credentials)
    
    @staticmethod
    def git_push(repo_path, remote="origin", branch=None, project_credentials=None):
        """推送更改到远程仓库"""
        command = f"git push {remote}"
        if branch:
            command += f" {branch}"
        return GitService.execute_git_command(repo_path, command, project_credentials)
    
    @staticmethod
    def git_pull(repo_path, remote="origin", branch=None, project_credentials=None):
        """从远程仓库拉取更改"""
        command = f"git pull {remote}"
        if branch:
            command += f" {branch}"
        return GitService.execute_git_command(repo_path, command, project_credentials)
    
    @staticmethod
    def git_branch(repo_path, project_credentials=None):
        """列出所有分支"""
        return GitService.execute_git_command(repo_path, "git branch", project_credentials)
    
    @staticmethod
    def git_checkout(repo_path, branch, project_credentials=None):
        """切换分支"""
        if not branch:
            return {"success": False, "message": "分支名不能为空", "data": None}
        
        return GitService.execute_git_command(repo_path, f"git checkout {branch}", project_credentials)
    
    @staticmethod
    def git_log(repo_path, count=10, project_credentials=None):
        """获取提交历史"""
        return GitService.execute_git_command(repo_path, f"git log --pretty=format:'%h|%an|%ad|%s' -n {count}", project_credentials)
        
    @staticmethod
    def git_clone(repo_url, target_path, project_id=None, credentials=None, max_retries=3, retry_delay=5):
        """
        克隆Git仓库到指定路径
        
        Args:
            repo_url: 仓库URL
            target_path: 克隆到的目标路径
            project_id: 项目ID，用于从数据库获取项目信息
            credentials: 凭证信息 {'access_token': 'xxx', 'git_username': 'xxx'}
            max_retries: 最大重试次数
            retry_delay: 重试间隔（秒）
        """
        retry_count = 0
        last_error = None
        
        try:
            # 如果提供了project_id，从数据库获取项目信息
            if project_id:
                try:
                    from models.project import Project
                    from database import db_session

                    project = db_session.query(Project).filter(Project.id == project_id).first()
                    if project:
                        if not credentials:
                            credentials = {}
                            
                        # 获取访问令牌
                        if project.git_access_token:
                            credentials['access_token'] = project.git_access_token
                            logger.info(f"从数据库获取到项目 {project_id} 的访问令牌")
                            
                        # 获取Git用户名
                        if project.git_username:
                            credentials['git_username'] = project.git_username
                            logger.info(f"从数据库获取到项目 {project_id} 的Git用户名: {project.git_username}")
                except Exception as e:
                    logger.error(f"从数据库获取项目信息失败: {str(e)}")
                    # 继续使用传入的credentials
        except Exception as e:
            logger.error(f"处理项目凭证时出错: {str(e)}")
            # 继续使用传入的credentials
        
        while retry_count < max_retries:
            try:
                if retry_count > 0:
                    logger.info(f"第 {retry_count + 1} 次尝试克隆，等待 {retry_delay} 秒...")
                    time.sleep(retry_delay)
                
                logger.info(f"开始克隆仓库 {repo_url} 到 {target_path}")
                
                # 检查和创建目录
                parent_dir = os.path.dirname(target_path)
                if not os.path.exists(parent_dir):
                    os.makedirs(parent_dir, exist_ok=True)
                    logger.info(f"创建了父目录: {parent_dir}")
                
                # 检查目录写入权限
                if os.path.exists(parent_dir):
                    try:
                        temp_file = os.path.join(parent_dir, ".write_test")
                        with open(temp_file, 'w') as f:
                            f.write("test")
                        os.remove(temp_file)
                        logger.info(f"已确认父目录 {parent_dir} 具有写入权限")
                    except Exception as e:
                        logger.error(f"目标目录不可写: {str(e)}")
                        return {
                            "success": False,
                            "message": f"目标目录 {parent_dir} 不可写: {str(e)}",
                            "data": None
                        }
                
                # 如果目录存在但不是git仓库，则删除
                if os.path.exists(target_path) and not os.path.exists(os.path.join(target_path, '.git')):
                    import shutil
                    try:
                        shutil.rmtree(target_path)
                    except Exception as e:
                        logger.error(f"删除现有目录失败: {str(e)}")
                        return {
                            "success": False,
                            "message": f"无法删除现有目录，该目录可能被其他程序占用: {target_path}",
                            "data": None
                        }
                
                # 准备环境变量
                env = os.environ.copy()
                env['GIT_TERMINAL_PROMPT'] = '0'
                env['GIT_ASKPASS'] = 'echo'
                
                # 处理克隆URL
                clone_url = repo_url
                if credentials and credentials.get('access_token'):
                    clone_url = GitService._get_auth_url(repo_url, credentials['access_token'])
                    logger.info("使用访问令牌认证")
                
                # 执行克隆命令
                clone_command = f"git clone {clone_url} {target_path}"
                if retry_count > 0:
                    # 添加一些 Git 配置来处理不稳定的网络
                    clone_command = f"git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=60 clone {clone_url} {target_path}"
                logger.info(f"执行克隆命令: {clone_command}")
                
                process = subprocess.Popen(
                    clone_command,
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    env=env
                )
                
                stdout, stderr = process.communicate()
                stdout = stdout.decode('utf-8', errors='replace')
                stderr = stderr.decode('utf-8', errors='replace')
                
                if process.returncode != 0:
                    error_msg = stderr.strip()
                    logger.error(f"克隆失败: {error_msg}")
                    last_error = error_msg
                    
                    # 检查是否是网络错误
                    if any(err in error_msg.lower() for err in ['502', '503', '504', 'timeout', 'timed out', 'connection refused', 'network']):
                        retry_count += 1
                        continue
                    
                    # 如果不是网络错误，直接返回失败
                    return {
                        "success": False,
                        "message": f"克隆失败: {error_msg}",
                        "data": {
                            "error_type": "git_error",
                            "retry_count": retry_count
                        }
                    }
                
                logger.info(f"仓库克隆成功: {target_path}")
                return {
                    "success": True,
                    "message": "仓库克隆成功",
                    "data": {"target_path": target_path}
                }
                
            except Exception as e:
                logger.error(f"克隆仓库时发生错误: {str(e)}")
                last_error = str(e)
                retry_count += 1
        
        # 所有重试都失败后返回最后一个错误
        return {
            "success": False,
            "message": f"克隆失败（已重试{max_retries}次）: {last_error}",
            "data": {
                "error_type": "network_error",
                "retry_count": retry_count
            }
        }

    @staticmethod
    def _sanitize_git_output(output):
        """移除Git输出中可能包含的敏感信息"""
        if not output:
            return output
            
        # 替换可能包含访问令牌的URL
        sanitized = re.sub(r'https://oauth2:[^@]+@', 'https://****:****@', output)
        
        # 替换可能包含用户名密码的URL
        sanitized = re.sub(r'https?://[^@]+@', 'https://****:****@', sanitized)
        
        # 替换常见的密码提示
        sanitized = re.sub(r'password for \'.*\':', 'password: [REDACTED]', sanitized)
        
        return sanitized 